1 /**
2    Property-based testing.
3  */
4 module unit_threaded.property;
5 
6 template from(string moduleName) {
7     mixin("import from = " ~ moduleName ~ ";");
8 }
9 
10 ///
11 class PropertyException : Exception {
12     this(in string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) @safe pure nothrow {
13         super(msg, file, line, next);
14     }
15 }
16 
17 /**
18    Check that bool-returning F is true with randomly generated values.
19  */
20 void check(alias F, int numFuncCalls = 100)(in uint seed = from!"std.random".unpredictableSeed,
21         in string file = __FILE__, in size_t line = __LINE__) @trusted {
22 
23     import unit_threaded.randomized.random : RndValueGen;
24     import unit_threaded.should : UnitTestException;
25     import std.conv : text;
26     import std.traits : ReturnType, Parameters, isSomeString;
27     import std.array : join;
28     import std.typecons : Flag, Yes, No;
29     import std.random : Random;
30 
31     static assert(is(ReturnType!F == bool),
32             text("check only accepts functions that return bool, not ", ReturnType!F.stringof));
33 
34     auto random = Random(seed);
35     auto gen = RndValueGen!(Parameters!F)(&random);
36 
37     auto input(Flag!"shrink" shrink = Yes.shrink) {
38         string[] ret;
39         static if (Parameters!F.length == 1 && canShrink!(Parameters!F[0])) {
40             auto val = gen.values[0].value;
41             auto shrunk = shrink ? val.shrink!F : val;
42             ret ~= shrunk.text;
43             static if (isSomeString!(Parameters!F[0]))
44                 ret[$ - 1] = `"` ~ ret[$ - 1] ~ `"`;
45         } else
46             foreach (ref valueGen; gen.values) {
47                 ret ~= valueGen.text;
48             }
49         return ret.join(", ");
50     }
51 
52     foreach (i; 0 .. numFuncCalls) {
53         bool pass;
54 
55         try {
56             gen.genValues;
57         } catch (Throwable t) {
58             throw new PropertyException("Error generating values\n" ~ t.toString, file, line, t);
59         }
60 
61         try {
62             pass = F(gen.values);
63         } catch (Throwable t) {
64             // trying to shrink when an exeption is thrown is too much of a bother code-wise
65             throw new UnitTestException(text("Property threw. Seed: ", seed,
66                     ". Input: ", input(No.shrink), ". Message: ", t.msg), file, line, t,);
67         }
68 
69         if (!pass) {
70             throw new UnitTestException(text("Property failed. Seed: ", seed,
71                     ". Input: ", input), file, line);
72         }
73     }
74 }
75 
76 /**
77    For values that unit-threaded doesn't know how to generate, test that the Predicate
78    holds, using Generator to come up with new values.
79  */
80 void checkCustom(alias Generator, alias Predicate)(int numFuncCalls = 100,
81         in string file = __FILE__, in size_t line = __LINE__) @trusted {
82 
83     import unit_threaded.should : UnitTestException;
84     import std.conv : text;
85     import std.traits : ReturnType;
86 
87     static assert(is(ReturnType!Predicate == bool),
88             text("check only accepts functions that return bool, not ", ReturnType!F.stringof));
89 
90     alias Type = ReturnType!Generator;
91 
92     foreach (i; 0 .. numFuncCalls) {
93 
94         Type object;
95 
96         try {
97             object = Generator();
98         } catch (Throwable t) {
99             throw new PropertyException("Error generating value\n" ~ t.toString, file, line, t);
100         }
101 
102         bool pass;
103 
104         try {
105             pass = Predicate(object);
106         } catch (Throwable t) {
107             throw new UnitTestException(text("Property threw. Input: ", object,
108                     ". Message: ", t.msg), file, line, t,);
109         }
110 
111         if (!pass) {
112             throw new UnitTestException("Property failed with input:" ~ object.text, file, line);
113         }
114     }
115 }
116 
117 private auto shrinkOne(alias F, int index, T)(T values) {
118     import std.stdio;
119     import std.traits;
120 
121     auto orig = values[index];
122     return shrink!((a) { values[index] = a; return F(values.expand); })(orig);
123 
124 }
125 
126 ///
127 @("Verify identity property for int[] succeeds")
128 @safe unittest {
129 
130     int numCalls;
131     bool identity(int[] a) pure {
132         ++numCalls;
133         return a == a;
134     }
135 
136     check!identity;
137     assert(numCalls == 100);
138 
139     numCalls = 0;
140     check!(identity, 10);
141     assert(numCalls == 10);
142 }
143 
144 ///
145 @("Explicit Gen")
146 @safe unittest {
147     import unit_threaded.randomized.gen;
148     import unit_threaded.should : UnitTestException;
149     import std.exception : assertThrown;
150 
151     check!((Gen!(int, 1, 1) a) => a == 1);
152     assertThrown!UnitTestException(check!((Gen!(int, 1, 1) a) => a == 2));
153 }
154 
155 private enum canShrink(T) = __traits(compiles, shrink!((T _) => true)(T.init));
156 
157 T shrink(alias F, T)(T value) {
158     import std.conv : text;
159 
160     assert(!F(value), text("Property did not fail for value ", value));
161 
162     T[][] oldParams;
163     return shrinkImpl!F(value, [value], oldParams);
164 }
165 
166 private T shrinkImpl(alias F, T)(in T value, T[] candidates, T[][] oldParams = [])
167         if (from!"std.traits".isIntegral!T) {
168     import std.algorithm : canFind, minPos;
169     import std.traits : isSigned;
170 
171     auto params = value ~ candidates;
172     if (oldParams.canFind(params))
173         return value;
174     oldParams ~= params;
175 
176     // if it suddenly starts passing we've found our boundary value
177     if (value < T.max && F(value + 1))
178         return value;
179     if (value > T.min && F(value - 1))
180         return value;
181 
182     bool stillFails(T attempt) {
183         if (!F(attempt) && !candidates.canFind(attempt)) {
184             candidates ~= attempt;
185             return true;
186         }
187 
188         return false;
189     }
190 
191     T[] attempts;
192     if (value != 0) {
193         static if (isSigned!T)
194             attempts ~= -value;
195         attempts ~= value / 2;
196     }
197     if (value < T.max / 2)
198         attempts ~= cast(T)(value * 2);
199     if (value < T.max)
200         attempts ~= cast(T)(value + 1);
201     if (value > T.min)
202         attempts ~= cast(T)(value - 1);
203 
204     foreach (attempt; attempts)
205         if (stillFails(attempt))
206             return shrinkImpl!F(attempt, candidates, oldParams);
207 
208     const min = candidates.minPos[0];
209     const max = candidates.minPos!"a > b"[0]; // maxPos doesn't exist before DMD 2.071.0
210 
211     if (!F(min))
212         return shrinkImpl!F(min, candidates, oldParams);
213     if (!F(max))
214         return shrinkImpl!F(max, candidates, oldParams);
215 
216     return candidates[0];
217 }
218 
219 static assert(canShrink!int);
220 
221 private T shrinkImpl(alias F, T)(T value, T[] candidates, T[][] oldParams = [])
222         if (from!"std.traits".isArray!T) {
223     if (value == [])
224         return value;
225 
226     if (value.length == 1) {
227         T empty;
228         return !F(empty) ? empty : value;
229     }
230 
231     auto fst = value[0 .. $ / 2];
232     auto snd = value[$ / 2 .. $];
233     if (!F(fst))
234         return shrinkImpl!F(fst, candidates);
235     if (!F(snd))
236         return shrinkImpl!F(snd, candidates);
237 
238     if (F(value[0 .. $ - 1]))
239         return value[0 .. $ - 1];
240     if (F(value[1 .. $]))
241         return value[1 .. $];
242 
243     if (!F(value[0 .. $ - 1]))
244         return shrinkImpl!F(value[0 .. $ - 1], candidates);
245     if (!F(value[1 .. $]))
246         return shrinkImpl!F(value[1 .. $], candidates);
247     return candidates[0];
248 }